package com.yhb.channel;

import com.yhb.channel.exception.*;
import com.yhb.channel.impl.ANYPAY.AnyPayChannel;
import com.yhb.channel.model.*;
import com.yhb.channel.provider.ChannelBaseService;
import com.yhb.channel.utils.ChannelJSONUtil;
import com.yhb.model.User;
import lombok.extern.log4j.Log4j;
import lombok.var;
import me.edzh.spring.bootstrap.utils.JSONUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.util.TextUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Log4j
public class ChannelDispatcher {

    @Autowired
    private SubChannelInfoList subChannelInfoList;

    @Autowired
    private ChannelBaseService channelBaseService;

    String TRYING_PAY_REDIS_PREFIX = "channel-polling-trying-pay-";
    String TRYING_REMIT_REDIS_PREFIX = "channel-trying-remit-";


    public void log(String ...msg) {
        log.info("ChannelDispatcher: " + StringUtils.join(msg, ","));
    }

    public void logParams(String methodName, String... params) {
        log.info("ChannelDispatcher::" + methodName + "->params=>" + StringUtils.join(params, "|"));
    }

    public void error(String msg, Throwable t) {
        log.error("ChannelDispatcher: " + msg, t);
    }

    /**
     * 根据支付参数 对支付通道进行排序 选出优先队列
     * @param payCard 支付卡
     * @param remitCard 代付卡
     * @param orderAmount 支付金额
     * @return 支付通道优先级列表
     */
    public PayChannel[] getChannelList(
            @NotNull IBankCard payCard, @NotNull IBankCard remitCard, @NotNull BigDecimal orderAmount) {
        logParams("getChannelList", payCard.getId(), remitCard.getId(), orderAmount.toString());
        PayChannel[] channels = subChannelInfoList.getChannelList(payCard, remitCard, orderAmount);
        if (channels.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无可用通道");
            log.error("getChannelList", t);
            throw t;
        }
        return channels;
    }

    /**
     * 获取符合条件的SubChannelInfo对象列表
     * @param payCard 支付卡
     * @param remitCard 代付卡
     * @param orderAmount 金额
     * @return SubChannelInfo对象列表
     */
    public SubChannelInfo[] getChannelInfoList(
            @NotNull IBankCard payCard, @NotNull IBankCard remitCard, @NotNull BigDecimal orderAmount) {
        logParams("getChannelInfoList", payCard.getId(), remitCard.getId(), orderAmount.toString());
        SubChannelInfo[] channelInfoList = subChannelInfoList.getChannelInfoList(payCard, remitCard, orderAmount);
        if (channelInfoList.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无可用通道");
            log.error("getChannelInfoList", t);
            throw t;
        }
        return channelInfoList;
    }

    /**
     * 获取第一优先的通道的绑定情况
     * @param payCard 支付卡
     * @param remitCard 代付卡
     * @param orderAmount 支付金额
     * @return 两张卡的绑定情况
     */
    public boolean getChannelReadyInfo(
            @NotNull IBankCard payCard, @NotNull IBankCard remitCard, @NotNull BigDecimal orderAmount) {
        logParams("getChannelReadyInfo", payCard.getId(), remitCard.getId(), orderAmount.toString());
        PayChannel[] payChannels = getChannelList(payCard, remitCard, orderAmount);
        PayChannel bestChannel = payChannels[0];
        boolean payCardIsOk = bestChannel.checkIfBound(payCard, false);
        boolean remitCardIsOk = bestChannel.checkIfBound(remitCard, true);
        log("绑定情况->" + payCardIsOk, String.valueOf(remitCardIsOk));
        return payCardIsOk && remitCardIsOk;
    }
    public boolean checkPayChannelSmsVerify(
            @NotNull IBankCard payCard, @NotNull IBankCard remitCard, @NotNull BigDecimal orderAmount) {
        logParams("getChannelReadyInfo", payCard.getId(), remitCard.getId(), orderAmount.toString());
        PayChannel[] payChannels = getChannelList(payCard, remitCard, orderAmount);
        PayChannel bestChannel = payChannels[0];
        return bestChannel.checkPayChannelSmsVerify();
    }

    public PayChannel getPayChannelByName(String channelName, SubChannelInfo[] channelInfoList) {
        PayChannel[] targetChannels = Arrays.stream(channelInfoList)
                .filter(channelInfo -> channelName.equals(channelInfo.getChannelName()))
                .map(SubChannelInfo::getPayChannel)
                .toArray(PayChannel[]::new);
        if (targetChannels.length == 0) {
            throw new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无可用通道");
        }
        return targetChannels[0];
    }

    public PayChannel getPayChannelByType(String channelType, SubChannelInfo[] channelInfoList) {
        PayChannel[] targetChannels = Arrays.stream(channelInfoList)
                .filter(channelInfo -> channelType.equals(channelInfo.getChannelType()))
                .map(SubChannelInfo::getPayChannel)
                .toArray(PayChannel[]::new);
        if (targetChannels.length == 0) {
            throw new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无可用通道");
        }
        return targetChannels[0];
    }

    public SubChannelInfo getPayChannelInfoByName(String channelName, SubChannelInfo[] channelInfoList) {
        SubChannelInfo[] subChannelInfos = Arrays.stream(channelInfoList)
                .filter(channelInfo -> channelName.equals(channelInfo.getChannelName()))
                .toArray(SubChannelInfo[]::new);
        if (subChannelInfos.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无此通道");
            log.error("getPayChannelInfoByName", t);
            throw t;
        }
        return subChannelInfos[0];
    }

    public SubChannelInfo getPayChannelInfoByName(String channelName) {
        SubChannelInfo[] subChannelInfos = Arrays.stream(this.subChannelInfoList.getFullChannelList())
                .filter(channelInfo -> channelName.equals(channelInfo.getChannelName()))
                .toArray(SubChannelInfo[]::new);
        if (subChannelInfos.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无此通道");
            log.error("getPayChannelInfoByName(Full)", t);
            throw t;
        }
        return subChannelInfos[0];
    }

    public SubChannelInfo getPayChannelInfoByType(String channelType, SubChannelInfo[] channelInfoList) {
        SubChannelInfo[] subChannelInfos = Arrays.stream(channelInfoList)
                .filter(channelInfo -> channelType.equals(channelInfo.getChannelType()))
                .toArray(SubChannelInfo[]::new);
        if (subChannelInfos.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无此通道");
            log.error("getPayChannelInfoByType", t);
            throw t;
        }
        return subChannelInfos[0];
    }

    public SubChannelInfo getPayChannelInfoByType(String channelType) {
        SubChannelInfo[] subChannelInfos = Arrays.stream(this.subChannelInfoList.getFullChannelList())
                .filter(channelInfo -> channelType.equals(channelInfo.getChannelType()))
                .toArray(SubChannelInfo[]::new);
        if (subChannelInfos.length == 0) {
            var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "无此通道");
            log.error("getPayChannelInfoByType(full)", t);
            throw t;
        }
        return subChannelInfos[0];
    }

    public SubChannelInfo getNextPayChannelByName(String channelName, SubChannelInfo[] subChannelInfos) {
        int index = Arrays.stream(subChannelInfos)
                        .map(SubChannelInfo::getChannelName)
                        .collect(Collectors.toList())
                        .indexOf(channelName);
        if (subChannelInfos.length - 1 >= index + 1) {
            return subChannelInfos[index + 1];
        }
        return null;
    }

    /**
     * 轮询通道 尝试支付 指定channelName或者channelType作为轮询起点
     * @param payCard 支付卡
     * @param remitCard 代付卡
     * @param orderAmount 支付金额
     * @param userFeeAmount 手续费
     * @param payRate 手续费率 eg 0.37
     * @param clientIP 客户端ip
     * @param channelName 轮询起点通道名
     * @param channelType 轮询起点通道别名
     * @return 若成功 返回支付订单流水号
     * @throws TryingBindCardException 如果当前最佳通道未绑定 返回绑定信息
     */
    public String pollingTryPay(
            String userId,
            IBankCard payCard, IBankCard remitCard,
            BigDecimal orderAmount, BigDecimal userFeeAmount, String payRate,
            String clientIP,
            String channelName, String channelType,
            String province,
            String city,
            String area,
            boolean ignoreCardAlreadyBound
    ) throws TryingBindCardExceptionWrapper {
        logParams("pollingTryPayWithChannelName",
                userId, payCard.getId(), remitCard.getId(),
                orderAmount.toString(), userFeeAmount.toString(),
                payRate, clientIP, province, city, area, channelName, channelType, String.valueOf(ignoreCardAlreadyBound)
        );
        channelBaseService.sessionVarProvider.set("ignoreCardAlreadyBound", ignoreCardAlreadyBound);
        checkUserValid(userId);
        SubChannelInfo[] channelInfoList = getChannelInfoList(payCard, remitCard, orderAmount);

        SubChannelInfo subChannelInfo = channelInfoList[0];

        if (!TextUtils.isEmpty(channelName)) {
            subChannelInfo = getPayChannelInfoByName(channelName, channelInfoList); // 如果没有channelType则用channelName
        }

        if (!TextUtils.isEmpty(channelType)) {
            subChannelInfo = getPayChannelInfoByType(channelType, channelInfoList); // 最优先channelType
        }

        try {
            log("尝试使用" + subChannelInfo.getChannelName() + "支付");
            return subChannelInfo
                    .getPayChannel()
                    .tryPay(userId, payCard, remitCard, orderAmount, userFeeAmount,
                            clientIP, payRate, province, city, area);
        } catch (TryingBindCardException e) {
            // 需要绑卡
            handleTryingBindCardException(e); // TryingBindCardException -> TryingBindCardExceptionWrapper
            return ""; // unreachable
        } catch (PayChannelDeadException e) {
            // 通道不可用 轮询下一通道
            String nameOfThisChannel = subChannelInfo.getChannelName(); // 当前通道名
            SubChannelInfo next = getNextPayChannelByName(nameOfThisChannel, channelInfoList);
            if (next == null) {
                var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "全部通道不可用，支付失败");
                log.error("pollingTryPayWithChannelName", t);
                throw t;
            }
            return this.pollingTryPay(userId, payCard, remitCard, orderAmount,
                    userFeeAmount, payRate, clientIP, next.getChannelName(), null, province, city, area, ignoreCardAlreadyBound);
        }
    }


    /**
     * 轮询通道 尝试支付 首次调用 不指定channelName或者channelType
     * @param userId 用户id
     * @param payCard 支付卡
     * @param remitCard 代付卡
     * @param orderAmount 支付金额
     * @param userFeeAmount 手续费
     * @param clientIP 客户端ip
     * @param commissionRate 手续费率 eg 0.37
     * @return 若成功 返回支付订单流水号
     * @throws TryingBindCardException 如果当前最佳通道未绑定 返回绑定信息
     */
    public String pollingTryPay(
            String userId,
            @NotNull IBankCard payCard,
            @NotNull IBankCard remitCard,
            @NotNull BigDecimal orderAmount,
            @NotNull BigDecimal userFeeAmount,
            String commissionRate,
            String clientIP,
            String province,
            String city,
            String area,
            boolean ignoreCardAlreadyBound
    ) throws TryingBindCardExceptionWrapper {
        // 兼容卡信息不完全的情况
        payCard = channelBaseService.iUserBankCardProvider.getBankCard(payCard.getId());
        remitCard = channelBaseService.iUserBankCardProvider.getBankCard(remitCard.getId());
        logParams("pollingTryPay",
                userId, payCard.getId(), remitCard.getId(),
                orderAmount.toString(), userFeeAmount.toString(),
                commissionRate, clientIP, province, city, area, String.valueOf(ignoreCardAlreadyBound)
        );
        return this.pollingTryPay(userId, payCard, remitCard,
                orderAmount, userFeeAmount, commissionRate, clientIP,
                null, null,
                province, city, area, ignoreCardAlreadyBound
        );
    }

    /**
     * 对pollingTryPay选出的最佳但未绑定的通道 验证短信 执行绑卡
     * @param userId 用户id
     * @param smsToken pollingTryPay抛出的token信息 内有支付信息
     * @param smsCode 短信验证码
     * @throws TryingBindCardExceptionWrapper 如果当前最佳通道的代付卡需要绑定且未绑定 返回绑定信息
     * @return 支付流水订单号
     */
    public String verifyPay(String userId,
                            String smsToken,
                            String smsCode,
                            String province,
                            String city,
                            String area) throws TryingBindCardExceptionWrapper {
        logParams("verifyPay", userId, smsCode, smsCode, province, province, city, area);
        checkUserValid(userId);
        String valStr = channelBaseService.redis.boundValueOps(TRYING_PAY_REDIS_PREFIX + smsToken).get();
        if (TextUtils.isEmpty(valStr)) {
            var t = new PayChannelException(ChannelConstant.INNER_WRONG, "绑卡短信Token过期");
            log.error("verifyPay", t);
            throw t;
        }
        TryPayPayLoadWrapper wrapper = ChannelJSONUtil.parseJson(valStr, TryPayPayLoadWrapper.class);
        SubChannelInfo[] subChannelInfos = subChannelInfoList.getFullChannelList();
        SubChannelInfo info = getPayChannelInfoByType(wrapper.getChannelType(), subChannelInfos);
        String payloadInfo = wrapper.getPayload();
        log("verifyPay::payload -> " + payloadInfo);
        TryPayPayload payload = ChannelJSONUtil.parseJson(payloadInfo, TryPayPayload.class);
        try {
            return info.getPayChannel().verifyPay(userId, smsCode, payload);
        } catch (PayChannelDeadException e) {
            // 超限等原因导致的绑完不可用
            SubChannelInfo[] subChannelInfoList = getChannelInfoList(e.getPayCard(), e.getRemitCard(), e.getAmount());
            SubChannelInfo next = getPayChannelInfoByType(wrapper.getChannelType(), subChannelInfoList);
            if (next == null) {
                var t = new PayChannelException(ChannelConstant.EMPTY_CHANNEL_LIST, "全部通道不可用，支付失败");
                log.error("verifyPay", t);
                throw t;
            }
            return this.pollingTryPay(userId, e.getPayCard(), e.getRemitCard(), e.getAmount(),
                    e.getUserFeeAmount(), e.getPayRate(), e.getClientIP(), next.getChannelName(), null,
                    province, city, area, true);
        } catch (TryingBindCardException e) {
            // verifyPay重放tryPay时发现 又需要绑卡了
            handleTryingBindCardException(e); // TryingBindCardException -> TryingBindCardExceptionWrapper
            return null; // @unreachable
        }
    }

    private void handleTryingBindCardException(@NotNull TryingBindCardException e) throws TryingBindCardExceptionWrapper {
        String msg = e.getMsg();
        String phoneNumber = e.getPhoneNumber();
        String channelNameOfUsedChannel = e.getChannelName();
        String channelTypeOfUsedChannel = e.getChannelType();
        String tryPayPayloadJson = e.getTryPayPayloadJson(); // 实际支付信息payload
        String token = UUID.randomUUID().toString();
        log("TryingBindCardException -> TryingBindCardExceptionWrapper", token, tryPayPayloadJson);
        TryPayPayLoadWrapper tryPayPayLoadWrapper = TryPayPayLoadWrapper.builder()
                .channelName(channelNameOfUsedChannel)
                .channelType(channelTypeOfUsedChannel)
                .payload(tryPayPayloadJson)
                .msg(msg)
                .phoneNumber(phoneNumber)
                .build();
        channelBaseService.redis
                .boundValueOps(TRYING_PAY_REDIS_PREFIX + token)
                .set(ChannelJSONUtil.toJson(tryPayPayLoadWrapper),
                        ChannelConstant.ONE_DAY, TimeUnit.MILLISECONDS); // 给verifyPay用的
        throw TryingBindCardExceptionWrapper.builder()
                .channelName(channelNameOfUsedChannel)
                .msg(msg)
                .phoneNumber(phoneNumber)
                .token(token)
                .build(); // 给业务用的
    }

    public void triggerAllChannelTicks() {
        log("triggerAllChannelTicks");
        SubChannelInfo[] subChannelInfos = subChannelInfoList.getFullChannelList();
        for (SubChannelInfo subChannelInfo : subChannelInfos) {
            try {
                subChannelInfo.getPayChannel().tick();
            } catch (Throwable t) {
                log.error("channel :" + subChannelInfo.getChannelType() + "tick failed", t);
            }
        }
    }

    public String remitWithChannelType(String channelType, String userId, @NotNull IBankCard remitCard, @Nullable IBankCard payCard, @NotNull BigDecimal amount, BigDecimal userFeeAmount) {
        logParams("remitWithChannelType", userId, channelType, remitCard.getId(), amount.toString());
        // 兼容卡信息不完全的情况
        remitCard = channelBaseService.iUserBankCardProvider.getBankCard(remitCard.getId());
        if (payCard != null) {
            payCard = channelBaseService.iUserBankCardProvider.getBankCard(payCard.getId());
        }
        checkUserValid(userId);
        String _channelType = channelType;
        IBankCard _payCard = payCard; // 清分时这里一定是null
        if (_channelType.startsWith(ChannelFactory.ANYPAY)) {
            // 特殊逻辑：AnyPayChannel通道清分时channelType后会跟着卡id用来获取payCard以便于准确清分
            var cardId = _channelType.split("\\$")[1];
            _channelType = _channelType.split("\\$")[0];
            _payCard = channelBaseService.iUserBankCardProvider.getBankCard(cardId);
        }
        if (_channelType.startsWith(ChannelFactory.SCALABLE)) {
            // 特殊逻辑：第三方通道清分时channelType后会跟着卡号用来获取payCard以便于准确清分
            var cardNumber = _channelType.split("\\$")[1];
            _channelType = _channelType.split("\\$")[0];
            _payCard = channelBaseService.iUserBankCardProvider.getBankCardByCardNumber(cardNumber);
        }
        SubChannelInfo[] subChannelInfos = subChannelInfoList.getFullChannelList();
        SubChannelInfo info = getPayChannelInfoByType(_channelType, subChannelInfos);
        try {
            return info.getPayChannel().remit(userId, remitCard, _payCard, amount, userFeeAmount,
                    false, null);
        } catch (RemitCardUnboundException ignored) {
            // remit调用参数active为false时 不会抛这个RemitCardUnboundException
        }
        return ""; // @unreachable
    }

    public List<ChannelBalance> queryUserBalance(String userId) {
        logParams("queryUserBalance", userId);
        checkUserValid(userId);
        List<SubChannelInfo> subChannelInfos = subChannelInfoList.getSubChannelInfos();

        return subChannelInfos.parallelStream()
                .filter(subChannelInfo -> !subChannelInfo.isNoNeedQueryBalance()) // 有些通道是不查余额的 eg: 对于北京畅捷11 12 13 14通道而言 他们的余额是放在一起的 所以跳过12 - 14通道的查询
                .map(SubChannelInfo::getPayChannel)
                .map(payChannel -> {
                    try {
                        return payChannel.getBalance(userId);
                    } catch (Throwable t) {
                        log.error("query balance err:" + t.getMessage(), t);
                        return new ArrayList<ChannelBalance>();
                    }
                })
                .reduce((acc, item) -> {
                    acc.addAll(item);
                    return acc;
                })
                .orElse(new ArrayList<>())
                ;
    }

    /**
     * 尝试将channelType通道提现出来 如果代付卡需要绑卡且未绑卡 则抛RemitCardUnboundException
     * ps: 对于此场景的remit单，标记为ACTIVE_REMIT而不是REMIT
     * @param channelType 通道type
     * @param user 用户
     * @param remitCard 提现目标卡
     * @param amount 金额
     * @param userFeeAmount 提现手续费
     * @return 提现订单号
     */
    public String tryRemit(
            @NotNull String channelType,
            @NotNull User user,
            @NotNull IBankCard remitCard,
            @Nullable IBankCard payCard,
            @NotNull BigDecimal amount, BigDecimal userFeeAmount,
            String clientIP,
            String payRate,
            String province,
            String city,
            String area
    ) throws TryBindRemitCardException {
        logParams("tryRemit", user.getId(), remitCard.getId(), amount.toString());
        checkUserValid(user.getId());

        // 兼容卡信息不完全的情况
        remitCard = channelBaseService.iUserBankCardProvider.getBankCard(remitCard.getId());
        if (payCard != null) {
            payCard = channelBaseService.iUserBankCardProvider.getBankCard(payCard.getId());
        }

        String _channelType = channelType;
        IBankCard _payCard = payCard;
        if (_channelType.startsWith(AnyPayChannel.CHANNEL_TYPE)) {
            // 特殊逻辑：AnyPayChannel通道channelType后会跟着卡id
            var cardId = _channelType.split("\\$")[1];
            _channelType = _channelType.split("\\$")[0];
            _payCard = channelBaseService.iUserBankCardProvider.getBankCard(cardId);
        }
        if (_channelType.startsWith(ChannelFactory.SCALABLE)) {
            // 特殊逻辑：第三方通道清分时channelType后会跟着卡号用来获取payCard以便于准确清分
            var cardNumber = _channelType.split("\\$")[1];
            _channelType = _channelType.split("\\$")[0];
            _payCard = channelBaseService.iUserBankCardProvider.getBankCardByCardNumber(cardNumber);
        }

        SubChannelInfo info = getPayChannelInfoByType(_channelType);
        TryBindCardExtendInfo extendInfo = TryBindCardExtendInfo.builder()
                .area(area)
                .city(city)
                .province(province)
                .payRate(payRate)
                .clientIP(clientIP)
                .build();
        try {
            return info.getPayChannel().remit(user.getId(), remitCard, _payCard, amount, userFeeAmount, true, extendInfo);
        } catch (RemitCardUnboundException e) {
            var payload = TryRemitPayload.builder().exception(e).build();
            String token = UUID.randomUUID().toString();
            channelBaseService.redis
                    .boundValueOps(TRYING_REMIT_REDIS_PREFIX + token)
                    .set(ChannelJSONUtil.toJson(payload),
                            ChannelConstant.ONE_DAY, TimeUnit.MILLISECONDS); // 给verifyRemit用的
            throw TryBindRemitCardException
                    .builder()
                    .token(token)
                    .build();
        }
    }

    /**
     * 对tryRemit进行校验支付
     * @param token 抛出的token
     * @param smsCode 短信验证码
     * @return 提现订单号
     */
    public String verifyRemit(
            @NotNull String token,
            @NotNull String smsCode
    ) {
        logParams("verifyRemit", token, smsCode);
        String valStr = channelBaseService.redis.boundValueOps(TRYING_REMIT_REDIS_PREFIX + token).get();
        var payload = ChannelJSONUtil.parseJson(valStr, TryRemitPayload.class);
        var e = payload.getException(); // todo e包含payCard
        var info = getPayChannelInfoByType(payload.getException().getChannelType());

        var payChannel = info.getPayChannel();
        String userId = e.getUserId();

        // 绑卡
        payChannel.bindCardConfirm(smsCode, userId, payload.getException().getSmsToken());

        // 提现
        try {
            return payChannel.remit(userId,
                    e.getRemitCard(), // todo 改为payCard
                    e.getRemitCard(),
                    e.getAmount(),
                    e.getUserFeeAmount(),
                    true,
                    null
            );
        } catch (RemitCardUnboundException ignored) {
            // 此时卡肯定绑过了 不会有RemitCardUnboundException
        }
        return ""; // @unreachable
    }

    public static void checkUserValid(String userId) {
//        if ("25d57db4-0d43-11ea-b54a-00163e0c8cb0".equals(userId)
//                || "2772a881-4659-11ea-91fc-00163e0c8cb0".equals(userId)) {
//            return;
//        }
//        throw new RuntimeException();
    }
}
